{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h1>Fantasy Basketball Part Two </h1>"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2> Introduction </h2>\n",
    "\n",
    "If you're familiar with fantasy basketball and worked through our **first example** you may have noticed that the lineup we solved for doesn't actually fit the criteria for many popular contests. It was meant as a ground-floor example into mathematical optimization, so make sure to dive into that one first for background on the problem. Additionally, we developed a predictive model to forecast each player's fantasy points, and we'll again use that forecast (without going through the modeling process again).\n",
    "\n",
    "In this follow-up example, we will extend the optimization model to fulfill the requirements of a typical [DraftKings](https://www.draftkings.com/help/rules/nba) lineup. Specifically, we'll need to change the model to:\n",
    "- Allow some players to be selected for up to two positions (e.g. LeBron James can fill the PF or SF position)\n",
    "- Increase the roster from five to eight players\n",
    "- Ensure the three new roster slots consist of one guard (SG or PG), one forward (SF or PF), and one utility player (any position)\n",
    "\n",
    "A quick recap of what we saw in the prior example: When selecting a 5-player lineup, there were *a lot* of possibilities; staying under a salary cap wasn't all that easy; and implementing intuitive decision rules can lead to suboptimal lineups -- providing a glimpse into why mathematical optimization should be part of everyone's analytics toolkit. \n",
    "\n",
    "**The repository for this project can be accessed by following this link:**\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2> Objective and Prerequisites </h2>\n",
    "\n",
    "As with the first example, the goal here is to build upon our findings of the previous example and select the optimal lineup of players of the National Basketball Association (NBA) that would produce the highest total of fantasy points, which are composed of a player's in-game stats. \n",
    "\n",
    "As mentioned above, we will make a lineup that meets DraftKings contest rules. There were over 2 million possible lineups when selecting a 5-player team that contained one of each position. Can you figure out the number of possible lineups for this example? Here's a hint: It's a lot. Remember that there are 25 PGs, 23 SFs, 22 SGs, 19 PFs, and 9 Cs. The answer (approximately) will be given a bit later. \n",
    "\n",
    "Also as mentioned, this example will use the same forecasting results as our fantasy basketball beginner's example, which used historical data from the 2016-2017 and 2017-2018 seasons to predict each player's fantasy points for games on 12/25/2017.\n",
    "\n",
    "This example assumes you have experience using Python for data manipulation and requires the installation of the following packages:\n",
    "\n",
    "- **pandas**: for data analysis and manipulation \n",
    "- **math**: for mathematical manipulations\n",
    "- **gurobipy**: for utilizing Gurobi to formulate and solve the optimization problem\n",
    "\n",
    "We'll also explore a few different ways to write summations and constraints in gurobipy, so you can find what works best for you. A quick note: Any output you see similar to <gurobi.Constr \\*Awaiting Model Update\\*> can be ignored and semi-colons have been added to cells to suppress that output. "
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2> Problem Statement and Solution Approach</h2>\n",
    "\n",
    "By formuating a model that allows some players to be assigned to more than one position and by also expanding the roster, we are making the model much more complex.  \n",
    "\n",
    "Our final lineup needs to include each of the following:\n",
    "- Point Guard (PG)\n",
    "- Shooting Guard (SG)\n",
    "- Shooting Forward (SF)\n",
    "- Power Forward (PF)\n",
    "- Center (C)\n",
    "- Guard (SG,PG)\n",
    "- Forward (SF,PF)\n",
    "- Utility (PG, SG, SF, PF, C)\n",
    "\n",
    "The solution of the problem consists of two components: 1) **fantasy points forecast** and 2) **lineup optimization**.\n",
    "\n",
    "We'll start by loading a dataset for the eligible players, showing their potential positions, salaries, as well as the forecasts for their upcoming performance. This information will act as an input to our optimization model which will guarantee that we satisfy the rules of the DraftKings contest while maximizing the total fantasy points of our team."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h3> Fantasy Points Forecast </h3>\n",
    "\n",
    "This section will be short as we are using the output of the predictive model from the first part of this example. We'll see that in this version some players have a *Main Position* and *Alternative Position*, which will let players fill different position slots if eligible. For example, James Harden can fill the PG or SG position, though Russel Westbrook is only available as a PG.\n",
    "\n",
    "We begin by loading the necessary libraries to solve our problem and importing the data from the predictive model in part one of the problem. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install gurobipy"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd                                       #importing pandas\n",
    "import math                                               #importing math\n",
    "import gurobipy as gp                                     #importing Gurobi"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Player</th>\n",
       "      <th>MainPosition</th>\n",
       "      <th>AlternativePosition</th>\n",
       "      <th>Team</th>\n",
       "      <th>Opp</th>\n",
       "      <th>Salary</th>\n",
       "      <th>PredictedFantasyPoints</th>\n",
       "      <th>Points/Salary Ratio</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Joel Embiid</td>\n",
       "      <td>C</td>\n",
       "      <td>NaN</td>\n",
       "      <td>PHI</td>\n",
       "      <td>NYK</td>\n",
       "      <td>9500</td>\n",
       "      <td>51.313689</td>\n",
       "      <td>5.401441</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>James Harden</td>\n",
       "      <td>PG</td>\n",
       "      <td>SG</td>\n",
       "      <td>HOU</td>\n",
       "      <td>OKC</td>\n",
       "      <td>11100</td>\n",
       "      <td>48.809577</td>\n",
       "      <td>4.397259</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>LeBron James</td>\n",
       "      <td>SF</td>\n",
       "      <td>PF</td>\n",
       "      <td>CLE</td>\n",
       "      <td>GSW</td>\n",
       "      <td>11000</td>\n",
       "      <td>48.149718</td>\n",
       "      <td>4.377247</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Russell Westbrook</td>\n",
       "      <td>PG</td>\n",
       "      <td>NaN</td>\n",
       "      <td>OKC</td>\n",
       "      <td>HOU</td>\n",
       "      <td>10900</td>\n",
       "      <td>44.007224</td>\n",
       "      <td>4.037360</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Kevin Durant</td>\n",
       "      <td>SF</td>\n",
       "      <td>PF</td>\n",
       "      <td>GSW</td>\n",
       "      <td>CLE</td>\n",
       "      <td>10500</td>\n",
       "      <td>43.438575</td>\n",
       "      <td>4.137007</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>Dario Saric</td>\n",
       "      <td>PF</td>\n",
       "      <td>C</td>\n",
       "      <td>PHI</td>\n",
       "      <td>NYK</td>\n",
       "      <td>6200</td>\n",
       "      <td>40.505486</td>\n",
       "      <td>6.533143</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Ben Simmons</td>\n",
       "      <td>PG</td>\n",
       "      <td>SF</td>\n",
       "      <td>PHI</td>\n",
       "      <td>NYK</td>\n",
       "      <td>9300</td>\n",
       "      <td>38.692817</td>\n",
       "      <td>4.160518</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>Kyle Kuzma</td>\n",
       "      <td>SF</td>\n",
       "      <td>PF</td>\n",
       "      <td>LAL</td>\n",
       "      <td>MIN</td>\n",
       "      <td>7300</td>\n",
       "      <td>38.201774</td>\n",
       "      <td>5.233120</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Jimmy Butler</td>\n",
       "      <td>SG</td>\n",
       "      <td>NaN</td>\n",
       "      <td>MIN</td>\n",
       "      <td>LAL</td>\n",
       "      <td>8400</td>\n",
       "      <td>37.873164</td>\n",
       "      <td>4.508710</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>Draymond Green</td>\n",
       "      <td>PF</td>\n",
       "      <td>C</td>\n",
       "      <td>GSW</td>\n",
       "      <td>CLE</td>\n",
       "      <td>7200</td>\n",
       "      <td>37.018949</td>\n",
       "      <td>5.141521</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>Paul George</td>\n",
       "      <td>SF</td>\n",
       "      <td>NaN</td>\n",
       "      <td>OKC</td>\n",
       "      <td>HOU</td>\n",
       "      <td>6800</td>\n",
       "      <td>36.398212</td>\n",
       "      <td>5.352678</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Karl-Anthony Towns</td>\n",
       "      <td>C</td>\n",
       "      <td>NaN</td>\n",
       "      <td>MIN</td>\n",
       "      <td>LAL</td>\n",
       "      <td>8900</td>\n",
       "      <td>36.188913</td>\n",
       "      <td>4.066170</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>Clint Capela</td>\n",
       "      <td>C</td>\n",
       "      <td>NaN</td>\n",
       "      <td>HOU</td>\n",
       "      <td>OKC</td>\n",
       "      <td>6600</td>\n",
       "      <td>33.490320</td>\n",
       "      <td>5.074291</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>John Wall</td>\n",
       "      <td>PG</td>\n",
       "      <td>NaN</td>\n",
       "      <td>WAS</td>\n",
       "      <td>BOS</td>\n",
       "      <td>8000</td>\n",
       "      <td>33.197563</td>\n",
       "      <td>4.149695</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>38</th>\n",
       "      <td>Jordan Bell</td>\n",
       "      <td>PF</td>\n",
       "      <td>C</td>\n",
       "      <td>GSW</td>\n",
       "      <td>CLE</td>\n",
       "      <td>4900</td>\n",
       "      <td>33.083296</td>\n",
       "      <td>6.751693</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Kyrie Irving</td>\n",
       "      <td>PG</td>\n",
       "      <td>NaN</td>\n",
       "      <td>BOS</td>\n",
       "      <td>WAS</td>\n",
       "      <td>8300</td>\n",
       "      <td>31.835863</td>\n",
       "      <td>3.835646</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>22</th>\n",
       "      <td>Jeff Teague</td>\n",
       "      <td>PG</td>\n",
       "      <td>NaN</td>\n",
       "      <td>MIN</td>\n",
       "      <td>LAL</td>\n",
       "      <td>6000</td>\n",
       "      <td>31.460451</td>\n",
       "      <td>5.243408</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>27</th>\n",
       "      <td>Steven Adams</td>\n",
       "      <td>C</td>\n",
       "      <td>NaN</td>\n",
       "      <td>OKC</td>\n",
       "      <td>HOU</td>\n",
       "      <td>5500</td>\n",
       "      <td>31.150428</td>\n",
       "      <td>5.663714</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>Klay Thompson</td>\n",
       "      <td>SG</td>\n",
       "      <td>NaN</td>\n",
       "      <td>GSW</td>\n",
       "      <td>CLE</td>\n",
       "      <td>6400</td>\n",
       "      <td>29.283897</td>\n",
       "      <td>4.575609</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>Al Horford</td>\n",
       "      <td>PF</td>\n",
       "      <td>C</td>\n",
       "      <td>BOS</td>\n",
       "      <td>WAS</td>\n",
       "      <td>6700</td>\n",
       "      <td>27.906772</td>\n",
       "      <td>4.165190</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "                Player MainPosition AlternativePosition Team  Opp  Salary  \\\n",
       "4          Joel Embiid            C                 NaN  PHI  NYK    9500   \n",
       "0         James Harden           PG                  SG  HOU  OKC   11100   \n",
       "1         LeBron James           SF                  PF  CLE  GSW   11000   \n",
       "2    Russell Westbrook           PG                 NaN  OKC  HOU   10900   \n",
       "3         Kevin Durant           SF                  PF  GSW  CLE   10500   \n",
       "19         Dario Saric           PF                   C  PHI  NYK    6200   \n",
       "5          Ben Simmons           PG                  SF  PHI  NYK    9300   \n",
       "12          Kyle Kuzma           SF                  PF  LAL  MIN    7300   \n",
       "8         Jimmy Butler           SG                 NaN  MIN  LAL    8400   \n",
       "13      Draymond Green           PF                   C  GSW  CLE    7200   \n",
       "15         Paul George           SF                 NaN  OKC  HOU    6800   \n",
       "6   Karl-Anthony Towns            C                 NaN  MIN  LAL    8900   \n",
       "17        Clint Capela            C                 NaN  HOU  OKC    6600   \n",
       "10           John Wall           PG                 NaN  WAS  BOS    8000   \n",
       "38         Jordan Bell           PF                   C  GSW  CLE    4900   \n",
       "9         Kyrie Irving           PG                 NaN  BOS  WAS    8300   \n",
       "22         Jeff Teague           PG                 NaN  MIN  LAL    6000   \n",
       "27        Steven Adams            C                 NaN  OKC  HOU    5500   \n",
       "18       Klay Thompson           SG                 NaN  GSW  CLE    6400   \n",
       "16          Al Horford           PF                   C  BOS  WAS    6700   \n",
       "\n",
       "    PredictedFantasyPoints  Points/Salary Ratio  \n",
       "4                51.313689             5.401441  \n",
       "0                48.809577             4.397259  \n",
       "1                48.149718             4.377247  \n",
       "2                44.007224             4.037360  \n",
       "3                43.438575             4.137007  \n",
       "19               40.505486             6.533143  \n",
       "5                38.692817             4.160518  \n",
       "12               38.201774             5.233120  \n",
       "8                37.873164             4.508710  \n",
       "13               37.018949             5.141521  \n",
       "15               36.398212             5.352678  \n",
       "6                36.188913             4.066170  \n",
       "17               33.490320             5.074291  \n",
       "10               33.197563             4.149695  \n",
       "38               33.083296             6.751693  \n",
       "9                31.835863             3.835646  \n",
       "22               31.460451             5.243408  \n",
       "27               31.150428             5.663714  \n",
       "18               29.283897             4.575609  \n",
       "16               27.906772             4.165190  "
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "player_predictions = pd.read_csv('https://raw.githubusercontent.com/Gurobi/modeling-examples/master/fantasy_basketball/results_target_advanced.csv')     #load processed dataset\n",
    "# player_predictions = pd.read_csv('results_target_advanced.csv')     #load processed dataset\n",
    "player_predictions.sort_values(by='PredictedFantasyPoints',ascending=False).head(20)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Have you figured out the total number of possible lineups yet? It's around $3.6 \\times 10^{11}$, which is a lot. \n",
    "\n",
    "This is where mathematical optimization and Gurobi are best utilized: To efficiently explore a huge decision space and provide a much needed tool for optimal decision-making."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h3> Optimal DraftKings Lineup Selection </h3>\n",
    "\n",
    "As we set up our optimization model, we first need to make some definitions. Some of this is the same as before, but as this example is a bit more complicated, we'll need to be a bit more thorough in some definitions.\n",
    "\n",
    "**Sets and Indices**\n",
    "\n",
    "$i$ is the index for the set of all players \n",
    "\n",
    "$j$ is the index for the set of basketball positions (PG,SG,SF,PF,C)\n",
    "\n",
    "**Input Parameters**\n",
    "\n",
    "$p_{i}$: the predicted fantasy points of player $i$ \n",
    "\n",
    "$s_{i}$: the salary of player $i$ \n",
    "\n",
    "$S$: our total available salary"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "players = player_predictions[\"Player\"].tolist()\n",
    "positions = player_predictions[\"MainPosition\"].unique().tolist()\n",
    "salaries = player_predictions[\"Salary\"].tolist()\n",
    "fantasypoints = player_predictions[\"PredictedFantasyPoints\"].tolist()\n",
    "S = 50000\n",
    "\n",
    "salary_dict = {players[i]: salaries[i] for i in range(len(players))}\n",
    "points_dict = {players[i]: fantasypoints[i] for i in range(len(players))}\n",
    "\n",
    "m = gp.Model()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Decision Variables**\n",
    "\n",
    "Since it is possible for certain players to fill one of two positions, we need to map each player to their eligible positions. Additionally, we need to add the position index to our decision variable. Instead of a binary variable (i.e. a variable that only takes the values of 0 or 1) $y_i$ we have $y_{i,j}$.\n",
    "\n",
    "$y_{i,j}$: This variable is equal to 1 if player $i$ is selected at position $j$; and 0 otherwise."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "mainposition = list(zip(player_predictions.Player, player_predictions.MainPosition))   \n",
    "alternativeposition = list(zip(player_predictions.Player, player_predictions.AlternativePosition))               \n",
    "indices = mainposition+alternativeposition\n",
    "player_pos_map = [t for t in indices if not any(isinstance(n, float) and math.isnan(n) for n in t)]\n",
    "\n",
    "y = m.addVars(player_pos_map, vtype=gp.GRB.BINARY, name=\"y\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Objective Function**\n",
    "\n",
    "The objective function of our problem is to maximize the total fantasy points of our lineup, same as the last time but using the differently indexed decision variable.\n",
    "\n",
    "\\begin{align}\n",
    " Max \\hspace{0.2cm} Z = \\sum_{i,j} p_{i} \\cdot y_{i,j}\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "m.setObjective(gp.quicksum(points_dict[i]*y[i,j] for i,j in player_pos_map), gp.GRB.MAXIMIZE)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Constraints**\n",
    "\n",
    "Our model still requires each of the primary basketball positions (PG, SG, SF, PF, C) to be filled. Last time we required exactly one of each, so these constraints were equalities (note that this also implied our roster size of five players). In this version of our model, we need *at least one* of each position since it is possible to use, for example, only one power forward (PF). \n",
    "\n",
    "For each position $j$:\n",
    "\\begin{align}\n",
    "\\sum_{i} y_{i,j} \\geq 1\n",
    "\\end{align}\n",
    "\n",
    "Here is where we have a couple of options in how to add these constraints. The first, intuitively, is to loop through the set of positions. The second uses a slightly different function to add the constraints (*addConstr* -> *addConstrs*). This function incorporates the for loop directly as an argument. **Running both will double up on the constraints in the model, which isn't best practice, so take a look at each approach and comment one out before running the cell.**"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "# option 1 for writing the above constraints\n",
    "for j in positions:\n",
    "    m.addConstr(gp.quicksum(y[i,j] for i, pos in player_pos_map if pos==j)>=1, name = \"pos\" + j)\n",
    "\n",
    "# option 2, a slightly more compact way of adding the same set of constraints\n",
    "m.addConstrs((gp.quicksum(y[i,j] for i, pos in player_pos_map if pos==j)>=1 for j in positions), name = \"pos\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now let's work with the additional slots of our new roster: the guard, forward, and utility slots. Here it's worth mentioning that there are different ways to formulate the same problem in mathematical optimization. Some approaches can be better than others and writing efficient models will become an important skill as you tackle larger and more complex problems. \n",
    "\n",
    "Let's first consider the additional slot for the guard position, which can be filled by a PG or a SG. Considering the overall roster we want to create, we have already guaranteed one PG and one SG (so two total guards). To make sure we get one more additional guard, a constraint needs to added that says the total number of guards needs to be *at least* three. \n",
    "\n",
    "\\begin{align}\n",
    "\\sum_{i} y_{i,j} \\geq 3, position\\space j \\space is\\space PG\\space or \\space SG\n",
    "\\end{align}\n",
    "\n",
    "The same needs to be done for forwards (SF or PF)\n",
    "\\begin{align}\n",
    "\\sum_{i} y_{i,j} \\geq 3, position\\space j \\space is\\space SF\\space or \\space PF\n",
    "\\end{align}\n",
    "\n",
    "It is important that we use inequalities here because of the utility position we'll address in a little bit. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "m.addConstr(gp.quicksum(y[i,j] for i, j in player_pos_map if (j=='PG' or j=='SG'))>=3)\n",
    "m.addConstr(gp.quicksum(y[i,j] for i, j in player_pos_map if (j=='SF' or j=='PF'))>=3);"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Now that we have a position index for our decision variable, we need to ensure each player is assigned to *at most* one position (either their primary or alternative, but not both). To do this we sum across each position $j$ for each player and limit that summation to one. \n",
    "\n",
    "For each player $i$,\n",
    "\\begin{align}\n",
    "\\sum_{j} y_{i,j} \\leq 1\n",
    "\\end{align}\n",
    "\n",
    "Here we'll use another way to sum over one of the variable's indices by appending *.sum* to the end and replacing the index we want to sum over with \"\\*\". This is useful because each player is not eligible for each position and this syntax automatically sums over the second index. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [],
   "source": [
    "m.addConstrs((y.sum(i, \"*\") <= 1 for i in players), name=\"max_one\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "A good exercise would be to rewrite other summations using this syntax and to try and the above constraint using quicksum. \n",
    "\n",
    "So far we have addressed seven of our eight lineup slots. Since the utility slot can have any position, all we need is to require the total number of players selected to be eight by setting the sum over all players and positions to that value.  \n",
    "\n",
    "\\begin{align}\n",
    "\\sum_{i,j} y_{i,j} = 8\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "m.addConstr(gp.quicksum(y[i,j] for i,j in player_pos_map) == 8, name=\"full_lineup\");"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Finally, we need to stay under the salary cap $S$, which in a typical DraftKings contest is $\\$50,000$:\n",
    "\n",
    "\\begin{align}\n",
    "\\sum_{i,j} s_{i} \\cdot y_{i,j} \\leq S\n",
    "\\end{align}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "cap = m.addConstr(gp.quicksum(salary_dict[i]*y[i,j] for i,j in player_pos_map) <= S, name=\"salary\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this last constraint, we stored it to be able to easily get information about this constraint after the model runs and the model is updated just as if we didn't store the constraint. Time to find the optimal lineup. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gurobi Optimizer version 11.0.2 build v11.0.2rc0 (win64 - Windows 10.0 (19045.2))\n",
      "\n",
      "CPU model: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz, instruction set [SSE2|AVX|AVX2|AVX512]\n",
      "Thread count: 4 physical cores, 8 logical processors, using up to 8 threads\n",
      "\n",
      "Optimize a model with 112 rows, 167 columns and 975 nonzeros\n",
      "Model fingerprint: 0x9c23fd93\n",
      "Variable types: 0 continuous, 167 integer (167 binary)\n",
      "Coefficient statistics:\n",
      "  Matrix range     [1e+00, 1e+04]\n",
      "  Objective range  [7e+00, 5e+01]\n",
      "  Bounds range     [1e+00, 1e+00]\n",
      "  RHS range        [1e+00, 5e+04]\n",
      "Found heuristic solution: objective 206.2597683\n",
      "Presolve removed 34 rows and 0 columns\n",
      "Presolve time: 0.00s\n",
      "Presolved: 78 rows, 167 columns, 779 nonzeros\n",
      "Variable types: 0 continuous, 167 integer (167 binary)\n",
      "Found heuristic solution: objective 268.7822348\n",
      "\n",
      "Root relaxation: objective 2.864073e+02, 68 iterations, 0.00 seconds (0.00 work units)\n",
      "\n",
      "    Nodes    |    Current Node    |     Objective Bounds      |     Work\n",
      " Expl Unexpl |  Obj  Depth IntInf | Incumbent    BestBd   Gap | It/Node Time\n",
      "\n",
      "     0     0  286.40727    0    2  268.78223  286.40727  6.56%     -    0s\n",
      "H    0     0                     283.6262215  286.40727  0.98%     -    0s\n",
      "     0     0  285.84410    0    6  283.62622  285.84410  0.78%     -    0s\n",
      "     0     0  285.84410    0    2  283.62622  285.84410  0.78%     -    0s\n",
      "     0     0  283.62622    0    6  283.62622  283.62622  0.00%     -    0s\n",
      "\n",
      "Cutting planes:\n",
      "  Cover: 1\n",
      "  RLT: 1\n",
      "\n",
      "Explored 1 nodes (82 simplex iterations) in 0.08 seconds (0.01 work units)\n",
      "Thread count was 8 (of 8 available processors)\n",
      "\n",
      "Solution count 3: 283.626 268.782 206.26 \n",
      "\n",
      "Optimal solution found (tolerance 1.00e-04)\n",
      "Best objective 2.836262214900e+02, best bound 2.836262214900e+02, gap 0.0000%\n"
     ]
    }
   ],
   "source": [
    "m.optimize()  # optimize our model"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notice the output above says we have 167 binary variables, which is much less than $98 \\cdot 5 = 490$ variables we'd have if we mapped all players to all basketball positions. Additionally, we would need more constraints to eliminate ineligible player/position combinations. While this wouldn't make much difference in this small example, formulating efficient models is a valuable skill in mathematical optimization. \n",
    "\n",
    "Let's display our optimal lineup. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Player</th>\n",
       "      <th>Assigned Position</th>\n",
       "      <th>Salary</th>\n",
       "      <th>Predicted Points</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Joel Embiid</td>\n",
       "      <td>C</td>\n",
       "      <td>9500</td>\n",
       "      <td>51.313689</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>Draymond Green</td>\n",
       "      <td>PF</td>\n",
       "      <td>7200</td>\n",
       "      <td>37.018949</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>Dario Saric</td>\n",
       "      <td>PF</td>\n",
       "      <td>6200</td>\n",
       "      <td>40.505486</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Jordan Bell</td>\n",
       "      <td>PF</td>\n",
       "      <td>4900</td>\n",
       "      <td>33.083296</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Jeff Teague</td>\n",
       "      <td>PG</td>\n",
       "      <td>6000</td>\n",
       "      <td>31.460451</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Jarrett Jack</td>\n",
       "      <td>PG</td>\n",
       "      <td>4600</td>\n",
       "      <td>27.780012</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Kyle Kuzma</td>\n",
       "      <td>SF</td>\n",
       "      <td>7300</td>\n",
       "      <td>38.201774</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Josh Hart</td>\n",
       "      <td>SG</td>\n",
       "      <td>3700</td>\n",
       "      <td>24.262564</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "           Player Assigned Position  Salary  Predicted Points\n",
       "0     Joel Embiid                 C    9500         51.313689\n",
       "2  Draymond Green                PF    7200         37.018949\n",
       "3     Dario Saric                PF    6200         40.505486\n",
       "5     Jordan Bell                PF    4900         33.083296\n",
       "4     Jeff Teague                PG    6000         31.460451\n",
       "6    Jarrett Jack                PG    4600         27.780012\n",
       "1      Kyle Kuzma                SF    7300         38.201774\n",
       "7       Josh Hart                SG    3700         24.262564"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "player_selections = []\n",
    "for v in m.getVars():\n",
    "    if (abs(v.x) > 1e-6):\n",
    "        player_selections.append(tuple(y)[v.index])\n",
    "        \n",
    "df = pd.DataFrame(player_selections, columns = ['Player','Assigned Position'])\n",
    "df = df.merge(pd.DataFrame(list(salary_dict.items()), columns=['Player', 'Salary']), left_on=['Player'], right_on=['Player'])\n",
    "lineup = df.merge(pd.DataFrame(list(points_dict.items()), columns=['Player', 'Predicted Points']), left_on=['Player'], right_on=['Player'])\n",
    "\n",
    "lineup.sort_values(by=['Assigned Position'])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Total fantasy score:  283.63\n",
      "Remaining salary:  600.0\n"
     ]
    }
   ],
   "source": [
    "print('Total fantasy score: ', round(m.objVal,2))\n",
    "print('Remaining salary: ', cap.Slack)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The last print statement uses the constraint we stored earlier. The *Slack* attribute of a constraint will show any gap between each side of the inequality. For this application, it's the gap between the salary cap and the amount of salary used in the optimal lineup, which shows there is $600 in unused salary. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Freeing default Gurobi environment\n"
     ]
    }
   ],
   "source": [
    "m.dispose()\n",
    "gp.disposeDefaultEnv()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<h2> Conclusion </h2>\n",
    "\n",
    "In this notebook we finished up a problem that started from a raw data set containing NBA player box score data and ended in, if our predictive model holds, an optimal fantasy basketball lineup. \n",
    "\n",
    "Specifically in this part, we:\n",
    "- Expanded the initial model to reflect the true complexity of creating a fantasy basketball lineup\n",
    "- Discovered multiple ways to add a set of constraints to a model\n",
    "- Utilized two summation commands\n",
    "- Used attributes of a model to get more information about the optimal solution\n",
    "\n",
    "Overall, this two-part example displayed how, even with the best predictive model, making optimal decisions is still a complicated exercise. Along with machine learning techniques, mathematical optimization is an essential part of a well-developed analytical toolbox."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
